#!/usr/bin/python3
# -*- coding: UTF-8 -*-

"""
@file_name: UDP_multicast.py
@author: WangQiXi
@create_date: 2021.03.01
@last_modify_date:2021.03.12
@modify_note:1、
@version:1.0.3
@email: 2309144172@qq.com
@function: UDP组播
"""
import gc
import socket


class UDPMulticast(object):
    """UDP组播类"""

    def __init__(self, multicast_address: str, send_port: int, receive_port: int):
        """
        :param multicast_address:组播组ip
        :param receive_port:数据接收端口号
        :param send_port:数据发送端口号
        """
        self.__multicast_address = multicast_address
        self.__receive_port = receive_port
        self.__send_port = send_port
        self.__any_ip = "0.0.0.0"  # 此字符串为监听所有网卡
        self.__udp_socket = None
        self.__host_ip = ""  # 本机地址
        self.__get_host_ip()  # 获取本机ip地址

    # 获取本机ip地址
    def __get_host_ip(self):
        """获取本机ip地址"""
        host_name = socket.gethostname()
        self.__host_ip = socket.gethostbyname(host_name)

    def establish_udp_multicast_socket(self, timeout: int):
        """
        建立组播套接字
        :param timeout:超时时间(单位：毫秒，msec)
        """
        try:
            # 创建UDP socket
            self.__udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
            # 允许端口复用
            self.__udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定监听多播数据包的端口，此处绑定置"0.0.0.0"
            self.__udp_socket.bind((self.__any_ip, self.__receive_port))
            # 声明该socket为多播类型
            self.__udp_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
            # 加入多播组，组地址由第三个参数制定
            self.__udp_socket.setsockopt(
                socket.IPPROTO_IP,
                socket.IP_ADD_MEMBERSHIP,
                socket.inet_aton(self.__multicast_address) + socket.inet_aton(self.__any_ip)
            )
            # 设置阻塞
            self.__udp_socket.setblocking(True)
            # 设置超时
            self.__udp_socket.settimeout(timeout)

            # 查看缓冲区大小
            recv_buff = self.__udp_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
            send_buff = self.__udp_socket.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
            # print(f'默认接收缓冲区大小：{recv_buff}。默认发送缓冲区大小：{send_buff}')
        # except Exception:
        #     raise Exception
        except OSError:  # 端口被占用时和ip地址为空时出现的异常
            raise OSError

    def send_data(self, message: bytes):
        """
        发送数据。
        :param message:消息
        """
        try:
            real_send_byte_num = self.__udp_socket.sendto(message, (self.__multicast_address, self.__send_port))
            # print(f"udp此次发送的字节数={real_send_byte_num}")
        except Exception:
            raise Exception
        # except AttributeError:  # self.__udp_socket未实例化之前调用此函数发生此错误
        #     raise AttributeError
        # except BlockingIOError:  # 设置套接字为非阻塞式收不到数据时发生此错误
        #     raise BlockingIOError

    def receive_data(self, buff_size: int):
        """
        接收数据。
        :param buff_size:缓冲区地址
        """
        receive_byte_data = bytearray()

        try:
            receive_byte_data, address = self.__udp_socket.recvfrom(buff_size)
            # 打印数据来源的地址，分析
            # print(f"接收的数据的地址={address}")
        except Exception:
            raise Exception
        # except AttributeError:  # self.__udp_socket未实例化之前调用此函数发生此错误
        #     raise AttributeError
        # except BlockingIOError:  # 设置套接字为非阻塞式收不到数据时发生此错误
        #     raise BlockingIOError

        return bytearray(receive_byte_data)

    def close_socket(self):
        """关闭套接字"""
        try:
            self.__udp_socket.close()
        except AttributeError:  # 套接字未建立时调用此函数会发生此错误
            raise AttributeError


# 解码字节数据方法
def decode_byte_data(byte_data: bytearray, start_code: bytes, decode_byte_num: int,
                     check_function, decode_function):
    """
    解码一种协议的字节数据；此函数对不符合协议的字节数据不作任何删减，调用方应增加对无效数据的处理语句。
    :param byte_data:字节数据
    :param start_code:起始码
    :param decode_byte_num:解码字节数
    :param check_function:校验函数；校验成功返回ture，失败返回false
    :param decode_function:解码函数；解码函数最好为一个信号发射函数。
    :return:返回为解码的字节数据
    """
    # 装载未解码的数据
    not_decode_byte_data = bytearray()
    start_code = bytearray(start_code)
    # 当字节数据长度大于等于解码字节数时，开始解码
    while len(byte_data) >= decode_byte_num:
        # 存储起始码在字节数据中的索引值
        start_code_index = 0
        # 寻找起始码
        try:
            start_code_index = byte_data.index(start_code)
        # 处理字节数据中无起始码的情况，跳出循环，结束函数
        except ValueError:
            break

        # 当起始码索引值之后的字节数据长度大于等于解码字节数，开始校验
        if len(byte_data[start_code_index:]) >= decode_byte_num:
            is_legal = check_function(byte_data[start_code_index:start_code_index + decode_byte_num])
            # 校验成功，解码
            if is_legal:
                decode_function(byte_data[start_code_index:start_code_index + decode_byte_num])
                # 解码完成，删除已解码数据并立即释放内存
                del byte_data[start_code_index:start_code_index + decode_byte_num]
                gc.collect()
                # byte_data = byte_data[:start_code_index] + byte_data[start_code_index + decode_byte_num:]
                # 循环
                # continue
            # 校验失败
            else:
                # 装载第一个起始码和起始码之前的未解码数据，防止数据丢失，回收内存
                not_decode_byte_data += byte_data[:start_code_index + 1]
                del byte_data[:start_code_index + 1]
                gc.collect()
                # 装载此索引之后的数据
                # byte_data = byte_data[start_code_index + 1:]
                # 循环
                # continue
        # 起始码及其后的数据不足以解码，跳出循环，退出
        else:
            break
    # 将未解码数据相加，防止数据丢失
    byte_data = not_decode_byte_data + byte_data
